[Android] 后台任务系列之JobScheduler


上次分析Android O广播的问题遗留了一个东西没提,那就是官方推荐使用的JobScheduler。这篇就简单了解一下这是个什么东西。

JobScheduler是什么

JobScheduler允许开发者创建在后台执行的job,当预置的条件被满足时,这些Job将会在后台被执行。

在Android开发中,我们会遇到很多这样的情况,比如在未来的某个时间点或者未来满足某种条件(比如插入电源或者连接WiFi)的情况下下去执行一些操作。在Android L上,Google提供了一个叫做JobScheduler的组件来帮助我们处理这种情况。

JobScheduler Api可以在我们的App中执行一些操作,这些操作将会在我们预置的一些条件被满足的时候被执行。和AlarmManager不一样,执行这些操作的时间并不是严格准确的。 JobScheduler会把一系列的job收集起来一起执行,这样既允许我们的job被执行,又能兼顾到手机电量的使用情况,达到节电的目的。

JobScheduler怎么用

JobScheduler的使用非常简单,只需要三步:

  • 创建JobService类
  • 创建JobInfo,通过builder设定Job的执行选项
  • 获取JobScheduler服务执行任务

下面按照这三步放一个简单代码示例。

JobService

JobService的作用是,在JobScheduler监测到系统状态达到对应启动条件时,会启动JobService执行任务。所以我们需要继承JobService创建一个自己的service,然后实现onStartJobonStopJob这两个方法。

public class JobSchedulerService extends JobService {

    private static final int MESSAGE_ID = 100;

    private Handler mJobHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            Log.i("WOW", "handle message!!!!");
            //请注意,我们手动调用了jobFinished方法。
            //当onStartJob返回true的时候,我们必须在合适时机手动调用jobFinished方法
            //否则该应用中的其他job将不会被执行
            jobFinished((JobParameters) msg.obj, false);
            //第一个参数JobParameter来自于onStartJob(JobParameters params)中的params,
            // 这也说明了如果我们想要在onStartJob中执行异步操作,必须要保存下来这个JobParameter。
            return true;
        }
    });

    // JobService运行在主线程 需要另外开启线程做耗时工作
    @Override
    public boolean onStartJob(JobParameters params) {
        Log.i("WOW", "onStartJob");
        // 注意到我们在使用Hanlder的时候把传进来的JobParameters保存下来了
        mJobHandler.sendMessage(Message.obtain(mJobHandler, MESSAGE_ID, params));

        // 返回false说明job已经完成  不是个耗时的任务
        // 返回true说明job在异步执行  需要手动调用jobFinished告诉系统job完成
        // 这里我们返回了true,因为我们要做耗时操作。
        // 返回true意味着耗时操作花费的事件比onStartJob执行的事件更长
        // 并且意味着我们会手动的调用jobFinished方法
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.i("WOW", "onStopJob");
        mJobHandler.removeMessages(MESSAGE_ID);

        // 当系统收到一个cancel job的请求时,并且这个job仍然在执行(onStartJob返回true),系统就会调用onStopJob方法。
        // 但不管是否调用onStopJob,系统只要收到取消请求,都会取消该job

        // true 需要重试
        // false 不再重试 丢弃job
        return false;
    }

然后在Manifest文件给service添加一个权限


JobInfo

JobInfo是对任务的描述,比如说需要监听哪些状态、重试策略、任务执行时间、是否持久化等等。 JobInfo.Builder的构造函数需要传入一个jobId,是Job的唯一标志,后续通过该jobId来取消Job。 通过Builder模式构造JobInfo

//Builder构造方法接收两个参数,第一个参数是jobId,每个app或者说uid下不同的Job,它的jobId必须是不同的
//第二个参数是我们自定义的JobService,系统会回调我们自定义的JobService中的onStartJob和onStopJob方法
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,
                             new ComponentName(this, JobSchedulerService.class));
builder.setMinimumLatency(2000) // 2s后执行
       .setOverrideDeadline(10000); // 最晚10s后执行

JobScheduler

通过服务获取JobScheduler执行任务即可。

JobScheduler mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
int result = mJobScheduler.schedule(builder.build());
if (result <= 0) {
  Log.i("WOW", "result is " + result + " Schedule failed");
}

JobScheduler API详解

JobService细节

在前面的代码注释已经有所说明

启动任务之后,会调用onStartJob方法,因为JobService运行在主线程,所以如果在任务开始时,如果要执行耗时的操作,就需要创建一个线程去做。

如果onStartJob执行的是不耗时的任务,就可以返回false,表示任务执行结束。

如果onStartJob起了一个线程执行耗时任务,就要返回true,表示任务还在执行,需要等任务真正结束后手动调用JobFinished()方法告诉系统任务已经结束。

JobInfo细节

JobInfo job=new JobInfo.Builder(i,componentName)

.setMinimumLatency(5000)//最小延时 5秒
.setOverrideDeadline(60000)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络
.setMinimumLatency(5000)//5秒 最小延时、
.setOverrideDeadline(60000)//maximum最多执行时间  
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)//免费的网络---wifi 蓝牙 USB
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络---
  /**
   设置重试/退避策略,当一个任务调度失败的时候执行什么样的测量采取重试。
    initialBackoffMillis:第一次尝试重试的等待时间间隔ms
    *backoffPolicy:对应的退避策略。比如等待的间隔呈指数增长。
    */
.setBackoffCriteria(long initialBackoffMillis, int backoffPolicy)
.setBackoffCriteria(JobInfo.MAX_BACKOFF_DELAY_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR)
.setPeriodic (long intervalMillis)//设置执行周期,每隔一段时间间隔任务最多可以执行一次。
.setPeriodic(long intervalMillis,long flexMillis)//在周期执行的末端有一个flexMiliis长度的窗口期,任务就可以在这个窗口期执行。
 //设置设备重启后,这个任务是否还要保留。需要权限:  RECEIVE_BOOT_COMPLETED
.setPersisted(boolean isPersisted);
.setRequiresCharging(boolean )//是否需要充电
.setRequiresDeviceIdle(boolean)//是否需要等设备出于空闲状态的时候
.addTriggerContentUri(uri)//监听uri对应的数据发生改变,就会触发任务的执行。
.setTriggerContentMaxDelay(long duration)//设置Content发生变化一直到任务被执行中间的最大延迟时间
//设置Content发生变化一直到任务被执行中间的延迟。如果在这个延迟时间内content发生了改变,延迟时间会重写计算。
.setTriggerContentUpdateDelay(long durationMilimms)

需要注意的是

setRequiredNetworkType(int networkType),setRequiresCharging(boolean requireCharging),setRequiresDeviceIdle(boolean requireIdle)

这几个方法可能会使得你的任务无法执行,除非调用setOverrideDeadline(long time)设置了最大延迟时间,使得你的任务在为满足条件的情况下也会被执行。

setMinimumLatency(long minLatencyMillis):这个方法指定我们的Job至少要多少毫秒之后执行,比如setMinimumLatency(5000),就表明我们这是了这个JobScheduler之后,这个Job至少要5秒之后执行,前五秒肯定是不会执行的。这个参数和setPeriodic互斥。两个同时设置会抛出异常。

setRequiredNetworkType(int networkType):来启动我们这个Job时所需要的网络类型,一共有三个值JobInfo.NETWORK_TYPE_NONE表明启动我们这个Job时不需要任何的网络连接;JobInfo.NETWORK_TYPE_ANY表明启动我们这个Job时只要连着网就可以,不要求网络类型。JobInfo.NETWORK_TYPE_UNMETERED表明启动我们这个Job时需要连接Wifi.

Android O 对JobScheduler的改进

  • 您现在可以将工作队列与计划作业关联。要将一个工作项添加到作业的队列中,请调用 JobScheduler.enqueue())。当作业运行时,它可以将待定工作从队列中剥离并进行处理。这种功能可以处理之前需要启动后台服务(尤其是实现 IntentService 的服务)的许多用例。

  • 您现在可以通过调用 JobInfo.Builder.setClipData()) 的方式将 ClipData 与作业关联。利用此选项,您可以将 URI 权限授予与作业关联,类似于这些权限传递到 Context.startService() 的方式。您也可以将 URI 权限授予用于工作队列上的 intent。

  • 计划作业现在支持多个新的约束条件:

源码分析

JobSchedulerService启动

首先因为知道JobScheduler是通过系统服务拿到的:

(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

所以可以想到,Android启动时所有的系统服务都是在SystemServer里启动:

//frameworks/base/services/java/com/android/server/SystemServer.java
mSystemServiceManager.startService(JobSchedulerService.class);

于是代码进入JobSchedulerService.java

//frameworks/base/services/core/java/com/android/server/job/JobSchedulerService.java
public final class JobSchedulerService extends com.android.server.SystemService
              implements StateChangedListener, JobCompletedListener {

官方代码注释里有一句说明:

The JobSchedulerService knows nothing about constraints, or the state of active jobs. It receives callbacks from the various controllers and completed jobs and operates accordingly.

就是JobSchedulerService对Job的状态和约束都不了解,完全是通过各种controller的回调去处理各种Job。

然后我们看其构造函数

public JobSchedulerService(Context context) {
  super(context);
  // 先创建在主线程的JobHandler
  mHandler = new JobHandler(context.getMainLooper());
  mConstants = new Constants(mHandler);
  // binder服务端
  mJobSchedulerStub = new JobSchedulerStub();
  mJobs = JobStore.initAndGet(this);

  // Create the controllers.
  mControllers = new ArrayList();
  mControllers.add(ConnectivityController.get(this));
  mControllers.add(TimeController.get(this));
  mControllers.add(IdleController.get(this));
  mControllers.add(BatteryController.get(this));
  mControllers.add(AppIdleController.get(this));
  mControllers.add(ContentObserverController.get(this));
  mControllers.add(DeviceIdleJobsController.get(this));
}

JobHandler

是一个在主线程运行的Handler,主要处理四个消息。

private class JobHandler extends Handler {

  public JobHandler(Looper looper) {
    super(looper);
  }

  @Override
  public void handleMessage(Message message) {
    synchronized (mLock) {
      if (!mReadyToRock) {
        return;
      }
    }
    switch (message.what) {
      case MSG_JOB_EXPIRED:
                ...
        break;
      case MSG_CHECK_JOB:
                ...
        break;
      case MSG_CHECK_JOB_GREEDY:
                ...
        break;
      case MSG_STOP_JOB:
                ...
        break;
    }
    maybeRunPendingJobsH();
    // Don't remove JOB_EXPIRED in case one came along while processing the queue.
    removeMessages(MSG_CHECK_JOB);
  }
  ...
}

Constants

这里面定义了一些与系统全局设置保持同步的常量。 这一类或其任何域的访问应该同时持有JobSchedulerService.mLock锁来完成。

private final class Constants extends ContentObserver {}

JobSchedulerStub

JobSchedulerStub作为实现接口IJobScheduler的binder服务端。

final class JobSchedulerStub extends IJobScheduler.Stub {}

JobStore.initAndGet

这个方法是创建了一个JobStore的单例由JobSchedulerService使用。

// frameworks/base/services/core/java/com/android/server/job/JobStore.java
/** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
static JobStore initAndGet(JobSchedulerService jobManagerService) {
  synchronized (sSingletonLock) {
    if (sSingleton == null) {
      sSingleton = new JobStore(jobManagerService.getContext(),
                                jobManagerService.getLock(),                                     Environment.getDataDirectory());
    }
    return sSingleton;
  }
}

JobStore

该类的作用是维护作业计划程序正在跟踪的作业主列表。 这些作业通过引用进行比较,因此此类中的任何函数都不应复制。 还处理持久作业的读/写。

创建一个JobStore实例,进行从磁盘读取文件。该方法会创建job目录以及jobs.xml文件, 以及从文件中读取所有的JobStatus。

private JobStore(Context context, Object lock, File dataDir) {
  mLock = lock;
  mContext = context;
  mDirtyOperations = 0;

  File systemDir = new File(dataDir, "system");
  File jobDir = new File(systemDir, "job");
  jobDir.mkdirs();
  // 创建/data/system/job/jobs.xml
  mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));

  mJobSet = new JobSet();

  readJobMapFromDisk(mJobSet);
}

readJobMapFromDisk方法从磁盘读取Job信息,先看一下Job.xml文件结构



  
  
  


可以看出这个xml中主要记录了每一个Job的jobid, JobService的名字,包名,以及触发该Job的一些条件信息。

对于xml解析就不分析了,这个思路都一样的,就是流程上是从xml中读取job的信息,然后利用这些信息创建JobStatus, JobStatus对象包含了JobInfo信息(Jobid,package,class),还有该Job的delay,deadline信息,用于schedule。JobStatus添加到mJobSet。

StateController

构造函数还创建了7个StateController:

类型 说明
ConnectivityController 注册监听网络连接状态的广播
TimeController 注册监听job时间到期的广播
IdleController 注册监听屏幕亮/灭,dream进入/退出,状态改变的广播
BatteryController 注册监听电池是否充电,电量状态的广播
AppIdleController 监听app是否空闲
ContentObserverController 通过ContentObserver监测content URIs的变化
DeviceIdleJobsController 根据doze状态为app设置约束。

前面提到过,JobSchedulerService是根据这些controller的回调处理Job的,所以简单看一下ConnectivityController

public interface StateChangedListener {
    public void onControllerStateChanged();
    public void onRunJobNow(JobStatus jobStatus);
    public void onDeviceIdleStateChanged(boolean deviceIdle);
}

public abstract class StateController {
    protected static final boolean DEBUG = JobSchedulerService.DEBUG;
    protected final Context mContext;
    protected final Object mLock;
    protected final StateChangedListener mStateChangedListener;

    public StateController(StateChangedListener stateChangedListener, Context context,
            Object lock) {
        mStateChangedListener = stateChangedListener;
        mContext = context;
        mLock = lock;
    }

    public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob);
    public void prepareForExecutionLocked(JobStatus jobStatus) {}
    public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate);
    public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {}
    public abstract void dumpControllerStateLocked(PrintWriter pw, int filterUid);
}
public class ConnectivityController extends StateController implements ConnectivityManager.OnNetworkActiveListener {

    public static ConnectivityController get(JobSchedulerService jms) {
        synchronized (sCreationLock) {
            if (mSingleton == null) {
                //单例模式
                mSingleton = new ConnectivityController(jms, jms.getContext());
            }
            return mSingleton;
        }
    }

    private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
        super(stateChangedListener, context);
        //注册监听网络连接状态的广播,且采用BackgroundThread线程
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        mContext.registerReceiverAsUser(
                mConnectivityChangedReceiver, UserHandle.ALL, intentFilter, null,
                BackgroundThread.getHandler());
        ConnectivityService cs =
                (ConnectivityService)ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
        if (cs != null) {
            if (cs.getActiveNetworkInfo() != null) {
                mNetworkConnected = cs.getActiveNetworkInfo().isConnected();
            }
            mNetworkUnmetered = mNetworkConnected && !cs.isActiveNetworkMetered();
        }
    }
}

所以可以知道,Android O以后禁止了一些广播的发送后,都是由这些Controller进行动态注册广播,由这些controller转交给JobScheduler进行处理。

流程控制

对遗留问题的说明

所以很明显,Android Framework对JobInfo已经设计好一些状态处理,比如说网络变化。所以这样不再用广播吊起更多App而引起性能问题了。

Ref

https://www.jianshu.com/p/caed2a5966fe


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录